# -*- coding: utf-8 -*-
'''
Created on 2019-01-01
微信公众号支付接口
@author: wangjinbin
'''

import time
import string
import random
import socket

import requests
import xmltodict

from .helper import smart_str, md5_constructor as md5

TIMEOUT = 5

try:
    SPBILL_CREATE_IP = socket.gethostbyname(socket.gethostname())
except:
    SPBILL_CREATE_IP = '127.0.0.1'


def generate_nonce_str(length=32):
    return ''.join(random.SystemRandom().choice(
        string.ascii_letters + string.digits) for _ in range(length))


def params_filter(params, delimiter='&', charset='utf-8',
                  excludes=['sign', 'sign_type']):
    ks = list(params.keys())
    ks.sort()
    newparams = {}
    prestr = ''
    if params.get('input_charset', None):
        charset = params['input_charset']
    for k in ks:
        v = params[k]
        k = smart_str(k, charset)
        if k not in excludes and v != '' and v is not None:
            newparams[k] = smart_str(v, charset)
            prestr += '%s=%s%s' % (k, newparams[k], delimiter)
    prestr = prestr[:-1]
    return newparams, prestr


# 生成签名
def build_mysign(prestr, key=None, sign_type='MD5'):
    if sign_type == 'MD5':
        prestr += '&key=%s' % str(key)
        return md5(prestr.encode(encoding='utf-8')).hexdigest().upper()
    return ''


class WeixinPay(object):
    """
    微信支付，父类
    """

    BASE_URL = 'https://api.mch.weixin.qq.com/'
    PAY_SOURCE = 'weixin'

    def __init__(self, appid, mch_id, sub_appid=None, sub_mch_id=None, *args, **kwargs):
        """
        微信支付接口
        :param appid: 公众账号ID
        :param mch_id: 商户号
        :param sub_appid: 可选，子appid，服务商模式下必填
        :param sub_mch_id: 可选，子商户号，服务商模式下必填
        :param mch_cert: 可选，商户证书路径 申请退款/订单撤销必须
        :param mch_key: 可选，商户证书私钥路径 申请退款/订单撤销必选
        :param notify_url: 可选 接收微信支付异步通知回调地址 统一下单接口必填
        :param partner_key: 商户支付Key
        """
        self.appid = appid
        self.mch_id = mch_id
        self.sub_appid = sub_appid
        self.sub_mch_id = sub_mch_id
        self.mch_cert = kwargs.get('mch_cert')
        self.mch_key = kwargs.get('mch_key')
        self.notify_url = kwargs.get('notify_url')
        self.partner_key = kwargs.get('partner_key')
        self.time_stamp = str(int(time.time()))

    def _full_url(self, path):
        return '%s%s' % (self.BASE_URL, path)

    def wxpay_sign_verify(self, post_data='', partner_key=None):
        # 验证签名
        _, prestr = params_filter(post_data, excludes=['sign'])
        mysign = build_mysign(prestr, key=partner_key)
        if mysign != post_data.get('sign'):
            return False
        else:
            return True

    def get_base_params(self):
        if self.sub_appid and self.sub_mch_id:
            params = {
                'appid': self.appid,  # 服务商ID
                'mch_id': self.mch_id,  # 商户号
                'sub_appid': self.sub_appid,  # 当前调起支付的公众号APPID
                'sub_mch_id': self.sub_mch_id,  # 子商户号
                'nonce_str': generate_nonce_str(),  # 随机字符串
            }
        else:
            params = {
                'appid': self.appid,  # 公众账号ID
                'mch_id': self.mch_id,  # 商户号
                'nonce_str': generate_nonce_str(),  # 随机字符串
            }
        return params

    def prepare_request(self, method, path, params):
        kwargs = {}
        _params = self.get_base_params()
        params.update(_params)
        newparams, prestr = params_filter(params)
        sign = build_mysign(prestr, self.partner_key)
        newparams['sign'] = sign
        xml_dict = {'xml': newparams}
        kwargs['data'] = smart_str(xmltodict.unparse(xml_dict))
        url = self._full_url(path)
        if self.mch_cert and self.mch_key:
            kwargs['cert'] = (self.mch_cert, self.mch_key)
        return method, url, kwargs

    def make_request(self, method, url, kwargs):
        req = requests.request(method, url, timeout=TIMEOUT, **kwargs)
        # xml to dict
        result = xmltodict.parse(req.content)
        # 只需要返回数据
        return result.get('xml')

    def order_query(self, transaction_id='', out_trade_no=''):
        """
        查询订单
        该接口提供所有微信支付订单的查询，商户可以通过查询订单接口主动查询订单状态，完成下一步的业务逻辑。
        https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_2
        :param out_trade_no: 可选，商户订单号，默认自动生成
        :param transaction_id: 可选，微信订单号 和out_trade_no 二选一
        :return: 返回的结果数据
        -----
        trade_state 订单状态
        SUCCESS—支付成功
        REFUND—转入退款
        NOTPAY—未支付
        CLOSED—已关闭
        REVOKED—已撤销（刷卡支付）
        USERPAYING--用户支付中
        PAYERROR--支付失败(其他原因，如银行返回失败)
        """
        path = 'pay/orderquery'
        params = dict(
            transaction_id=transaction_id,
            out_trade_no=out_trade_no
        )
        method, url, kwargs = self.prepare_request('POST', path, params)
        return self.make_request(method, url, kwargs)

    def order_close(self, out_trade_no):
        """
        关闭订单
        以下情况需要调用关单接口：商户订单支付失败需要生成新单号重新发起支付，要对原订单号调用关单，避免重复支付；系统下单后，用户支付超时，系统退出不再受理，避免用户继续，请调用关单接口。
        注意：订单生成后不能马上调用关单接口，最短调用时间间隔为5分钟。
        https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_3
        :param out_trade_no: 可选，商户订单号，默认自动生成
        :return: 返回的结果数据
        """
        path = 'pay/closeorder'
        params = dict(
            out_trade_no=out_trade_no
        )
        method, url, kwargs = self.prepare_request('POST', path, params)
        return self.make_request(method, url, kwargs)

    def refund(self, out_refund_no, total_fee, refund_fee,
               out_trade_no='', transaction_id='',
               device_info='', refund_fee_type='CNY', refund_desc='', refund_account='', notify_url=''):
        """
        申请退款
        当交易发生之后一段时间内，由于买家或者卖家的原因需要退款时，卖家可以通过退款接口将支付款退还给买家，微信支付将在收到退款请求并且验证成功之后，按照退款规则将支付款按原路退到买家帐号上。
        https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4
        :param out_refund_no: 商户退款单号
        :param total_fee: 总金额
        :param refund_fee: 退款金额
        :param transaction_id: 可选，商户订单号，默认自动生成
        :param out_trade_no: 可选，微信订单号 以上两个二选一
        :param refund_fee_type: 可选 货币类型，符合ISO 4217标准的三位字母代码，默认人民币：CNY
        :param device_info: 可选，终端设备号(门店号或收银设备ID)，注意：PC网页或公众号内支付请传"WEB"
        :param refund_desc: 可选，退款原因
        :param refund_account: 可选，退款资金来源
        :param notify_url: 可选，退款结果通知url
        :return: 返回的结果数据
        """
        path = 'secapi/pay/refund'
        params = dict(
            out_refund_no=out_refund_no,
            total_fee=total_fee,
            refund_fee=refund_fee,
            transaction_id=transaction_id,
            out_trade_no=out_trade_no,
            refund_fee_type=refund_fee_type,
            refund_desc=refund_desc,
            refund_account=refund_account,
            notify_url=notify_url
        )
        method, url, kwargs = self.prepare_request('POST', path, params)
        return self.make_request(method, url, kwargs)

    def refund_query(self, out_trade_no='', transaction_id='',
                     out_refund_no='', refund_id='', device_info=''):
        """
        退款查询接口
        提交退款申请后，通过调用该接口查询退款状态。退款有一定延时，用零钱支付的退款20分钟内到账，银行卡支付的退款3个工作日后重新查询退款状态。
        注意：如果单个支付订单部分退款次数超过20次请使用退款单号查询
        https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_5
        :param transaction_id: 可选，微信订单号
        :param out_trade_no: 可选，商户订单号，默认自动生成
        :param out_refund_no: 可选，商户退款单号
        :param refund_id: 可选，微信退款单号 以上四个四选一
        :param device_info: 可选，终端设备号(门店号或收银设备ID)，注意：PC网页或公众号内支付请传"WEB"
        :return: 返回的结果数据
        ----------
        退款状态：
        SUCCESS     退款成功
        FAIL        退款失败
        PROCESSING  退款处理中
        NOTSURE     未确定，需要商户原退款单号重新发起
        CHANGE      转入代发，退款到银行发现用户的卡作废或者冻结了，
                    导致原路退款银行卡失败，资金回流到商户的现金帐号，
                    需要商户人工干预，通过线下或者财付通转账的方式进行退款。
        """
        path = 'pay/refundquery'
        params = dict(
            out_trade_no=out_trade_no,
            transaction_id=transaction_id,
            out_refund_no=out_refund_no,
            refund_id=refund_id,
            device_info=device_info,
        )
        method, url, kwargs = self.prepare_request('POST', path, params)
        return self.make_request(method, url, kwargs)

    def download_bill(self, bill_date, bill_type):
        """
        功能：下载对账单
        :param bill_date: 账单日期，格式yyyymmdd
        :return: 返回的结果数据
        -----
        bill_type取值如下：
        ALL，返回所有订单信息，默认值；
        SUCCESS，返回当日成功支付的订单；
        REFUND，返回当日退款订单；
        RECHARGE_REFUND，返回当日充值退款订单（相比其他对账单多一栏“返还手续费”）
        """
        path = 'pay/downloadbill'
        params = dict(
            bill_date=bill_date,
            bill_type=bill_type
        )
        method, url, kwargs = self.prepare_request('POST', path, params)
        return requests.request(method, url, timeout=TIMEOUT, **kwargs)


class MiniAppPay(WeixinPay):
    """微信公众号支付"""

    # 统一下单
    def unifiedorder(self, body='', out_trade_no='', total_fee='', openid='', sub_openid='',
                     detail='', attach='', time_start='', time_expire='',
                     goods_tag='', product_id='', limit_pay='', device_info='',
                     fee_type='CNY', spbill_create_ip=SPBILL_CREATE_IP,
                     trade_type='JSAPI', notify_url=''):
        """
        统一下单接口
        https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_sl_api.php?chapter=9_1
        :param trade_type: 交易类型，取值如下：JSAPI，NATIVE，APP，WAP, MWEB
        :param body: 商品描述
        :param total_fee: 总金额，单位分
        :param notify_url: 接收微信支付异步通知回调地址
        :param client_ip: 可选，APP和网页支付提交用户端ip，Native支付填调用微信支付API的机器IP
        :param openid: 可选，用户在商户appid下的唯一标识。trade_type=JSAPI，此参数必传
        :param out_trade_no: 可选，商户订单号，默认自动生成
        :param detail: 可选，商品详情
        :param attach: 可选，附加数据，在查询API和支付通知中原样返回，该字段主要用于商户携带订单的自定义数据
        :param fee_type: 可选，符合ISO 4217标准的三位字母代码，默认人民币：CNY
        :param time_start: 可选，订单生成时间，默认为当前时间
        :param time_expire: 可选，订单失效时间，默认为订单生成时间后两小时
        :param goods_tag: 可选，商品标记，代金券或立减优惠功能的参数
        :param product_id: 可选，trade_type=NATIVE，此参数必传。此id为二维码中包含的商品ID，商户自行定义
        :param device_info: 可选，终端设备号(门店号或收银设备ID)，注意：PC网页或公众号内支付请传"WEB"
        :param spbill_create_ip: 调用接口的机器Ip地址
        :param limit_pay: 可选，指定支付方式，no_credit--指定不能使用信用卡支付
        :return: 返回的结果数据
        """

        _notify_url = notify_url or self.notify_url
        path = 'pay/unifiedorder'
        params = dict(
            body=body,  # 商品描述
            total_fee=total_fee,  # 总金额
            out_trade_no=out_trade_no,  # 商户订单号
            openid=openid,  # trade_type=JSAPI，此参数必传，用户在商户appid下的唯一标识。
            sub_openid=sub_openid, # trade_type=JSAPI，此参数必传，用户在子商户appid下的唯一标识。openid和sub_openid可以选传其中之一，如果选择传sub_openid,则必须传sub_appid。
            fee_type=fee_type,  # 货币类型
            spbill_create_ip=spbill_create_ip,  # 终端IP
            notify_url=_notify_url,  # 通知地址
            trade_type=trade_type,  # 交易类型
            device_info=device_info,  # 设备号
            detail=detail,  # 商品详情
            attach=attach,  # 附加数据
            time_start=time_start,  # 交易起始时间
            time_expire=time_expire,  # 交易结束时间
            goods_tag=goods_tag,  # 商品标记
            product_id=product_id,  # 商品ID
            limit_pay=limit_pay,  # 指定支付方式
        )
        method, url, kwargs = self.prepare_request('POST', path, params)
        return self.make_request(method, url, kwargs)


class MicroPay(WeixinPay):
    '''
    微信付款码支付
    '''

    def micropay(self, order_name, total_fee, auth_code):
        """
        功能：付款码支付
        描述：提交付款码支付
        :param order_name: 订单code
        :param total_fee:总额
        :param auth_code:授权码。付款码支付授权码，设备读取用户微信中的条码或者二维码信息。（注：用户刷卡条形码规则：18位纯数字，以10、11、12、13、14、15开头）
        :return:
        """

        path = 'pay/micropay'
        _host_name = socket.gethostname()
        spbill_create_ip = socket.gethostbyname(_host_name)
        params = dict(
            out_trade_no=order_name,
            total_fee=int(total_fee * 100),  # 单位是分，必须是整数
            auth_code=auth_code,
            spbill_create_ip=spbill_create_ip,
            body=order_name
        )
        method, url, kwargs = self.prepare_request('POST', path, params)
        return self.make_request(method, url, kwargs)

    def order_reverse(self, order_name):
        """
        功能：撤销订单
        描述：支付交易返回失败或支付系统超时，调用该接口撤销交易。如果此订单用户支付失败，微信支付系统会将此订单关闭；如果用户支付成功，微信支付系统会将此订单资金退还给用户。
            注意：7天以内的交易单可调用撤销，其他正常支付的单如需实现相同功能请调用申请退款API。提交支付交易后调用【查询订单API】，没有明确的支付结果再调用【撤销订单API】。
            调用支付接口后请勿立即调用撤销订单API，建议支付后至少15s后再调用撤销订单接口。
        :param order_name: 订单code
        :return:
        """

        path = 'secapi/pay/reverse'
        params = dict(
            out_trade_no=order_name
        )
        method, url, kwargs = self.prepare_request('POST', path, params)
        return self.make_request(method, url, kwargs)
